﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Data;
using System.Diagnostics;

namespace MoneyWentWhere.DataModel.Binding
{
    public class TransactionList : BindingList<Transaction>
    {
        public Account Account { get; internal set; }

        // For serialization
        public TransactionList() { } 

        public TransactionList(Account account) : base() 
        {
            Account = account; 
        }

        protected override void RemoveItem(int index)
        {
            RemoveItem(index, true);
        }

        public void RemoveItem(int index, bool removeTransfer)
        {
            Transaction item = this[index];

            // Remove associated transfer transaction
            if (removeTransfer && item.TransferToTx != null)
            {
                item.TransferRemove();
            }

            item.Account = null;
            base.RemoveItem(index);

            // Calculate properties
            int endIndex = CompensateInNextReconcile(index, -item.Amount);
            UpdateAccountBalance(index, endIndex);

        }

        #region BUGFIX: AddNew
        Transaction _newItem;
        protected override object AddNewCore()
        {
            _newItem = base.AddNewCore() as Transaction;
            return _newItem;
        }
        public override void CancelNew(int itemIndex)
        {
            _newItem = null;
            base.CancelNew(itemIndex);
        }
        public override void EndNew(int itemIndex)
        {
            _newItem = null;
            base.EndNew(itemIndex);
        }
        #endregion

        protected override void InsertItem(int index, Transaction item)
        {
            // BUGFIX: called on a different object while the user edits
            // the special last row for adding new items
            if (_newItem != null && _newItem != item) { return; }

            if (base.Contains(item))
            {
                throw new InvalidOperationException("Transaction already present: " + item);
            }

            // If date is not determined, choose it so index is respected
            if (item.Date == DateTime.MaxValue) 
            { 
                item.Date = DateBasedOnIndex(index); 
            }
            else
            {
                index = IndexBasedOnDate(item.Date, index);
            }

            // Do the insert
            base.InsertItem(index, item);
            item.Account = Account;

            // Calculate properties
            int endIndex = CompensateInNextReconcile(index + 1, item.Amount);
            UpdateAccountBalance(index, endIndex); ;
        }

        // Item changed, refresh list params as needed
        internal void OnDateChanged(Transaction item)
        {
            // TODO: replace with stored index, but that's a bit messy for now.
            // Could do a binary search in the meantime... see if this is a bottleneck.
            int index = IndexOf(item); 
            if (index < 0) { return; }

            // Re-add at a different point
            RemoveItem(index, false);
            Insert(index, item);
        }

        // Amount changed, recalculate balances
        internal void OnAmountChanged(Transaction item, decimal amountChange)
        {
            int index = IndexOf(item); 
            if (index < 0) { return; }

            int endIndex = CompensateInNextReconcile(index + 1, amountChange);
            UpdateAccountBalance(index, endIndex);
        }

        void UpdateAccountBalance(int startIndex)
        {
            UpdateAccountBalance(startIndex, -1);
        }

        void UpdateAccountBalance(int startIndex, int endIndex)
        {
            if (Account == null) { return; }

            if (endIndex == -1) { endIndex = Count - 1; }

            decimal balance = 0;
            if (startIndex > 0) { balance = this[startIndex - 1].AccountBalance; }

            // Update transactions in the range
            for (int i = startIndex; i <= endIndex; i++)
            {
                balance = balance + this[i].Amount;
                this[i].SetAccountBalance(balance);
            }

            // Update the account balance
            if (Count == 0) { Account.Balance = 0; }
            else { Account.Balance = this[Count - 1].AccountBalance; }
        }

        /// <summary>
        /// Decrement the amount changed from the next reco Tx.
        /// (e.g. if money was added to Tx, take it out from the reco)
        /// If there is no next reco, does nothing.
        /// </summary>
        /// <param name="startIndex"></param>
        /// <param name="amountChange"></param>
        /// <returns>Index of changed reco Tx, -1 if none</returns>
        int CompensateInNextReconcile(int startIndex, decimal amountChange)
        {
            for (int i = startIndex; i < Count; i++)
            {
                if (this[i].Kind == TransactionKind.Reconcile)
                {
                    // Account for the change into reconciliation
                    this[i].SetAmount(this[i].Amount - amountChange);
                    return i;
                }
            }
            return -1;
        }


        /// <summary>
        /// Returns a date based on the suggested index transaction should be 
        /// inserted in. Biased to suggest DateTime.Now or the index of previous
        /// transaction when possible.
        /// </summary>
        /// <param name="suggestedIndex"></param>
        /// <returns></returns>
        DateTime DateBasedOnIndex(int suggestedIndex)
        {
            Transaction prev = null;
            int prevIndex = suggestedIndex - 1;
            if (prevIndex >= 0 && prevIndex < Count) { prev = this[prevIndex]; }

            Transaction next = null;
            int nextIndex = suggestedIndex; // Item not inserted yet
            if (nextIndex >= 0 && nextIndex < Count) { next = this[nextIndex]; }

            // Try to return previous tx date in most cases
            if (prev != null) { return prev.Date; }
            if (next != null) { return next.Date; }
            return DateTime.Now.Date;
        }

        /// <summary>
        /// Get the index for the to-be-inserted item based on the date.
        /// Also takes into account suggestedIndex (tries to put it as close)
        /// </summary>
        /// <param name="suggestedDate"></param>
        /// <param name="suggestedIndex"></param>
        /// <returns></returns>
        int IndexBasedOnDate(DateTime suggestedDate, int suggestedIndex)
        {
            int newIndex = suggestedIndex;

            // Move back if needed
            int prevIdx = suggestedIndex - 1;
            while (prevIdx >= 0 && this[prevIdx].Date > suggestedDate)
            {
                --prevIdx;
            }
            newIndex = prevIdx + 1;
            if (newIndex != suggestedIndex) { return newIndex; }

            // Move forward if needed
            int nextIdx = suggestedIndex; // Item not inserted yet
            while (nextIdx < Count && this[nextIdx].Date < suggestedDate)
            {
                ++nextIdx;
            }
            newIndex = nextIdx;
            if (newIndex != suggestedIndex) { return newIndex; }

            // Suggested index was OK after all
            return suggestedIndex;
        }
    }

}
